// SPDX-License-Identifier: Apache-2.0
import type {
	BigTypedArray,
	EVENT_ALL,
	Fn,
	IDeref,
	ILength,
	Keys,
	TypedArray,
	Values,
} from "@thi.ng/api";
import type { WasmBridge } from "./bridge.js";

export const EVENT_MEMORY_CHANGED = "memory-changed";
export const EVENT_PANIC = "panic";

export type BridgeEventType =
	| typeof EVENT_MEMORY_CHANGED
	| typeof EVENT_PANIC
	| typeof EVENT_ALL;

export type BigIntArray = bigint[] | BigInt64Array | BigUint64Array;

export type ReadonlyWasmString = IDeref<string> &
	ILength & {
		readonly addr: number;
	};

export interface WasmTypeBase {
	/**
	 * Base address in linear WASM memory.
	 */
	readonly __base: number;
	/**
	 * Obtain as byte buffer
	 */
	readonly __bytes: Uint8Array;
}

/**
 * Helper type to extract field names of structs/unions based on
 * {@link WasmTypeBase} (e.g. those generated by thi.ng/wasm-api-bindgen).
 */
export type WasmTypeKeys<T extends WasmTypeBase> = Keys<
	Omit<T, Keys<WasmTypeBase>>
>;

export interface WasmType<T> {
	/**
	 * Type alignment (in bytes)
	 */
	readonly align: number;
	/**
	 * Type size (in bytes)
	 */
	readonly size: number;
	/**
	 * Takes a start address and number of items. Returns an array of memory
	 * mapped type instances (see {@link WasmType.instance}).
	 *
	 * @param base
	 * @param num
	 * @returns
	 */
	instanceArray: (base: number, num: number) => T[];
	/**
	 * Returns a memory-mapped type instance for given start address.
	 *
	 * @param base
	 */
	instance: (base: number) => T;
}

export type WasmTypeConstructor<T> = Fn<IWasmMemoryAccess, WasmType<T>>;

/**
 * Common interface for WASM/JS child APIs which will be used in combination
 * with a parent {@link WasmBridge}.
 *
 * @remarks
 * The generic type param is optional and only used if the API is requiring
 * certain exports declared by WASM module.
 */
export interface IWasmAPI<T extends WasmExports = WasmExports> {
	/**
	 * Called by {@link WasmBridge.init} to initialize all child APIs (async)
	 * after the WASM module has been instantiated. If the method returns false
	 * the overall initialization process will be stopped/terminated.
	 *
	 * @param parent
	 */
	init(parent: WasmBridge<T>): Promise<boolean>;
	/**
	 * Returns an object of this child API's declared WASM imports. Be aware
	 * imports from all child APIs will be merged into a single flat namespace,
	 * it's recommended to use naming prefixes to avoid clashes.
	 */
	getImports(): WebAssembly.ModuleImports;
}

export interface WasmModuleSpec<T extends WasmExports = WasmExports> {
	/**
	 * The unique ID for grouping the WASM imports of this module. MUST be the
	 * same as used by the native side of the module.
	 */
	id: string;
	/**
	 * Optional array of other {@link WasmModuleSpec}s which this module depends
	 * on (element order is irrelevant). Used to construct a module dependency
	 * graph and the correct initialization order of modules. The core module
	 * (defined via {@link WasmBridge} w/ unique ID: `wasmapi`) is always
	 * considered an implicit dependency, will be initialized first and MUST NOT
	 * be stated here.
	 */
	deps?: WasmModuleSpec<T>[];
	/**
	 * Factory function to pre-instantiate the API module.
	 *
	 * @remarks
	 * Note: All modules will only be fully initialized at a later point via
	 * {@link WasmBridge.instantiate} or {@link WasmBridge.init} and each
	 * modules own {@link IWasmAPI.init} method.
	 */
	factory: Fn<WasmBridge<T>, IWasmAPI<T>>;
}

/**
 * Base interface of exports declared by the WASM module. At the very least, a
 * compatible module needs to export its memory and the functions defined in
 * this interface.
 *
 * @remarks
 * This interface is supposed to be extended with the concrete exports defined
 * by your WASM module and is used as generic type param for {@link WasmBridge}
 * and any {@link IWasmAPI} bridge modules. These exports can obtained via
 * {@link WasmBridge.exports} where they will be stored during the execution of
 * {@link WasmBridge.init}.
 */
export interface WasmExports {
	/**
	 * The WASM module's linear memory buffer. The `WasmBridge` automatically
	 * creates various typed views of that memory (i.e. u8, u16, u32, f32 etc.)
	 */
	memory: WebAssembly.Memory;
	/**
	 * Implementation specific WASM memory allocation function. If successful
	 * returns address of new memory block, or zero if unsuccessful.
	 *
	 * @remarks
	 * #### Zig
	 *
	 * Using the supplied Zig bindings (see `/zig/lib.zig`), it's the
	 * user's responsibility to define a public `WASM_ALLOCATOR` in the root
	 * source file to enable allocations, e.g. using the
	 * [`std.heap.GeneralPurposeAllocator`](https://ziglang.org/documentation/master/#Choosing-an-Allocator)
	 * (which also automatically handles growing the WASM memory). However, as
	 * mentioned, the underlying mechanism is purposefully left to the actual
	 * WASM-side implementation. If no allocator is defined this function
	 * returns zero, which in turn will cause {@link WasmBridge.allocate} to
	 * throw an error.
	 *
	 * #### C/C++
	 *
	 * Using the supplied C bindings (see `/include/wasmapi.h`), it's the user's
	 * responsibility to enable allocation support by defining the
	 * `WASMAPI_MALLOC` symbol (and compiling the WASM module with a malloc
	 * implementation).
	 */
	_wasm_allocate(numBytes: number): number;
	/**
	 * Implementation specific function to free a previously allocated chunk of
	 * of WASM memory (allocated via {@link WasmExports._wasm_allocate}, also
	 * see remarks for that function).
	 *
	 * @param addr
	 * @param numBytes
	 */
	_wasm_free(addr: number, numBytes: number): void;
}

export type MemorySlice = [addr: number, len: number];

export type MemoryViewType =
	| "i8"
	| "u8"
	| "i16"
	| "u16"
	| "i32"
	| "u32"
	| "i64"
	| "u64"
	| "f32"
	| "f64";

export interface MemoryViewTypeMap
	extends Record<MemoryViewType, TypedArray | BigTypedArray> {
	u8: Uint8Array;
	u8c: Uint8ClampedArray;
	i8: Int8Array;
	u16: Uint16Array;
	i16: Int16Array;
	u32: Uint32Array;
	i32: Int32Array;
	i64: BigInt64Array;
	u64: BigUint64Array;
	f32: Float32Array;
	f64: Float64Array;
}

export type MemoryView = Values<MemoryViewTypeMap>;

export interface IWasmMemoryAccess {
	i8: Int8Array;
	u8: Uint8Array;
	i16: Int16Array;
	u16: Uint16Array;
	i32: Int32Array;
	u32: Uint32Array;
	i64: BigInt64Array;
	u64: BigUint64Array;
	f32: Float32Array;
	f64: Float64Array;

	/**
	 * Initializes and/or updates the various typed WASM memory views (e.g.
	 * after growing the WASM memory and the previous buffer becoming detached).
	 */
	ensureMemory(): void;

	/**
	 * Attempts to grow the WASM memory by an additional `numPages` (64KB/page)
	 * and if successful updates all typed memory views to use the new
	 * underlying buffer.
	 *
	 * @param numPages
	 */
	growMemory(numPages: number): void;

	/**
	 * Attempts to allocate `numBytes` using the exported WASM core API function
	 * {@link WasmExports._wasm_allocate} (implementation specific) and returns
	 * start address of the new memory block. If unsuccessful, throws an
	 * {@link OutOfMemoryError}. If `clear` is true, the allocated region will
	 * be zero-filled.
	 *
	 * @remarks
	 * See {@link WasmExports._wasm_allocate} docs for further details.
	 *
	 * @param numBytes
	 * @param clear
	 */
	allocate(numBytes: number, clear?: boolean): MemorySlice;

	/**
	 * Frees a previous allocated memory region using the exported WASM core API
	 * function {@link WasmExports._wasm_free} (implementation specific). The
	 * `numBytes` value must be the same as previously given to
	 * {@link IWasmMemoryAccess.allocate}.
	 *
	 * @remarks
	 * This function always succeeds, regardless of presence of an active
	 * allocator on the WASM side or validity of given arguments.
	 *
	 * @param slice
	 */
	free(slice: MemorySlice): void;

	/**
	 * Reads UTF-8 encoded string from given address and optional byte length.
	 * The default length is 0, which will be interpreted as a zero-terminated
	 * string. Returns string.
	 *
	 * @param addr
	 * @param len
	 */
	getString(addr: number, len?: number): string;

	/**
	 * Encodes given string as UTF-8 and writes it to WASM memory starting at
	 * `addr`. By default the string will be zero-terminated and only `maxBytes`
	 * will be written. Returns the number of bytes written (excluding final
	 * sentinel, if any).
	 *
	 * @remarks
	 * An error will be thrown if the encoded string doesn't fully fit into the
	 * designated memory region (also note that there might need to be space for
	 * the additional sentinel/termination byte).
	 *
	 * @param str
	 * @param addr
	 * @param maxBytes
	 * @param terminate
	 */
	setString(
		str: string,
		addr: number,
		maxBytes: number,
		terminate?: boolean
	): number;
}

/**
 * Core API of WASM imports defined by the {@link WasmBridge}. The same
 * functions are declared as bindings in `/zig/lib.zig`. **Also see this
 * file for documentation of each function...**
 *
 * @remarks
 * Zig API:
 * https://github.com/thi-ng/umbrella/blob/develop/packages/wasm-api/zig/lib.zig
 */
export interface CoreAPI extends WebAssembly.ModuleImports {
	printI8: Fn<number, void>;
	printU8: Fn<number, void>;
	printU8Hex: Fn<number, void>;
	printI16: Fn<number, void>;
	printU16: Fn<number, void>;
	printU16Hex: Fn<number, void>;
	printI32: Fn<number, void>;
	printU32: Fn<number, void>;
	printU32Hex: Fn<number, void>;
	printI64: Fn<bigint, void>;
	printU64: Fn<bigint, void>;
	printU64Hex: Fn<bigint, void>;
	printF32: Fn<number, void>;
	printF64: Fn<number, void>;
	_printI8Array: (addr: number, len: number) => void;
	_printU8Array: (addr: number, len: number) => void;
	_printI16Array: (addr: number, len: number) => void;
	_printU16Array: (addr: number, len: number) => void;
	_printI32Array: (addr: number, len: number) => void;
	_printU32Array: (addr: number, len: number) => void;
	_printI64Array: (addr: number, len: number) => void;
	_printU64Array: (addr: number, len: number) => void;
	_printF32Array: (addr: number, len: number) => void;
	_printF64Array: (addr: number, len: number) => void;
	printStrZ: (addr: number) => void;
	_printStr: (addr: number, len: number) => void;
	printHexdump: (addr: number, len: number) => void;
	debug: () => void;
	_panic: (addr: number, len: number) => void;
	timer: () => number;
	epoch: () => bigint;
}
